import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { CronExpression, SchedulerRegistry } from '@nestjs/schedule';
import { CronJob } from 'cron';
import { InjectRepository } from '@nestjs/typeorm';
import { File } from './entities/file.entity';
import { In, Repository } from 'typeorm';
import * as OSS from 'ali-oss';
import axios from 'axios';
import { getRandomStr, md5, ulid } from 'src/utils';
const dayjs = require('dayjs');
const sharp = require('sharp');

interface TaskName {
  name: string;
}
@Injectable()
export class FileService {
  private client: OSS;

  constructor(
    @InjectRepository(File) private fileRepository: Repository<File>,
    private configService: ConfigService,
    private schedulerRegistry: SchedulerRegistry,
  ) {
    const config = {
      region: this.configService.get('OSS_REGION'),
      accessKeyId: this.configService.get('OSS_KI'),
      accessKeySecret: this.configService.get('OSS_KS'),
      bucket: this.configService.get('OSS_BUCKET'),
      authorizationV4: true, // V4签名
      timeout: 1000 * 60 * 5, // OSS超时时间5分钟
    };

    this.client = new OSS(config);
  }

  /**
   * getBufferFromUrl - 获取buffer
   *
   * @param {string} url - 有效的http地址
   * @memberof FileService
   */
  getBufferFromUrl = async (url: string): Promise<Buffer> => {
    try {
      if (!url) return Promise.resolve(null);

      const response = await axios.get(url, {
        responseType: 'stream',
        timeout: 1000 * 60 * 3,
      }); // 3分钟超时，队列重试时间是3分钟
      // 读取响应流
      const buffer: Buffer = await new Promise((resolve, reject) => {
        const chunks = [];
        response.data.on('data', (chunk: any) => {
          chunks.push(chunk);
        });
        response.data.on('end', () => {
          resolve(Buffer.concat(chunks));
        });
        response.data.on('error', (err: any) => {
          reject(err);
        });
      });

      return buffer;
    } catch (e) {
      let msg = '';
      let showError = true;
      if ('ECONNABORTED' === e?.code) {
        msg = '连接被终止';
        showError = false;
      } else if ('ETIMEDOUT' === e?.code) {
        msg = '连接超时';
        showError = false;
      } else if ('ENETUNREACH' === e?.code) {
        msg = '网络不可达';
        showError = false;
      } else if (e?.response) {
        if (500 === e?.response?.status) {
          showError = false;
          msg = `目标服务器异常`;
        } else {
          msg = `请求成功，响应异常`;
        }
      } else if (e?.request) {
        msg = `请求已发起，未收到响应`;
      } else {
        msg = `未知异常`;
      }
    }

    return Promise.resolve(null);
  };

  /**
   * 将文件转存到oss上，转换失败会报错误
   *
   * @param {any[]} urls 需要上传的文件地址，可能是url也可能是buffer
   * @param {string} userId 用户id
   * @param {boolean} [toMax=false] 文件是否需要保存至最大天数，true:最大天数，false:1天
   * @param {string} [suffix='jpeg'] 转换后的文件后缀，默认jpeg
   * @memberof FileService
   */
  transToOss = async (urls:any[], userId:string, toMax:boolean=false, suffix:string='jpeg') => {
      const today = dayjs();
      const dateDir = today.format('YYYYMMDD');
      const expire = today.add(toMax ? this.configService.get('OSS_MAX_DATE') : 1, 'day').format('YYYYMMDD');
      const isHttp = (str:string) => str && 'string' === typeof(str) && (str.startsWith("http://") || str.startsWith("https://"));
      const isBase64 = (str:string) => str && str.length && 0 === str.length%4; // 长度4的倍数 && 最多只能包含两个 =，并且这些 = 只能出现在末尾

      if(!userId) return [];
      const ossUrls = [];
      for(let i=0;i<urls.length;i++) {
        const url = urls[i] || '';
        let buffer:Buffer = null;

        if(Buffer.isBuffer(url)) {
            buffer = url;
        }else{
            if(isHttp(url)) { // 小心开头的空格
                buffer = await this.getBufferFromUrl(url);
            }else if(isBase64(url)) {
                buffer = Buffer.from(url, 'base64');
            }else{
                throw new Error(`无法获取文件类型：![url, base64]`);
            }
        }

        if(!buffer) throw new Error(`OSS转存buffer为空，无法转存`);

        const fileName = `${dateDir}/${userId}/${getRandomStr(8, false)}.${suffix}`;
        // 上传到OSS
        await this.client.put(fileName, buffer);
        // 数据入库，主要是为了定时任务删除
        await this.fileRepository.save({url: fileName, expire});

        ossUrls.push(fileName);
      }

      return ossUrls;
  };
  
  /**
   * 移动文件到指定目录
   *
   * @param {*} url 文件地址
   * @param {*} dir 文件目标目录
   * @memberof FileService
   */
  moveToDir = async (url: any, dir: string): Promise<string> => {
    const fileName = url.split('/').pop();

    try {
      const exist: boolean = await this.fileExist(url);
      if (!exist) return '';
      await this.client.copy(`${dir}/${fileName}`, url); // 不去掉开头的/就拷贝失败，不知道为啥
      // 文件移动后，原文件会在定时任务中删除

      return `${dir}/${fileName}`;
    } catch (e) {}

    return '';
  };


  /**
   * 上传文件
   * 上传的文件默认都可以访问，24:00进行批量删除
   * 上传成功就更新file数据库 { id, url, expire, created_at, updated_at }
   *
   * @param {*} file
   * @param {boolean} [isCommon=false] true:保留90天，false:保留当天23:59:59
   * @return {*}  {Promise<string>}
   * @memberof FileService
   */
  async uploadFile(file: any, isCommon: boolean = false): Promise<string> {
    try {
      const today = dayjs();
      const dateDir = today.format('YYYYMMDD');
      const expire = today
        .add(isCommon ? this.configService.get('OSS_MAX_DATE') : 1, 'day')
        .format('YYYYMMDD');
      const fileName = file.originalname;
      const suffix = fileName.split('.').pop();
      const newFileName = `/${dateDir}/${ulid().replace(/-/g, '')}.${suffix}`;

      // result = {name: '20250401/xxx.png', url, res: {xxx}}
      const result = await this.client.put(newFileName, file.buffer);
      // 数据入库，主要是为了定时任务删除
      await this.fileRepository.save({ url: newFileName, expire, payload: JSON.stringify({size: file.size, originalname: file.originalname}) });

      return result.name;
    } catch (e) {
      throw new BadRequestException(`文件上传失败： ${e.message}`);
    }
  }

  /**
   * 更新file的有效期
   *
   * @param {string} url 文件地址
   * @param {string} toMax 是否更新为最大天数，true:最大天数，false:1天
   * @memberof FileService
   */
  async updateExpire(url: string, toMax: boolean = true): Promise<boolean> {
    if (!url) return false;
    const exist = await this.fileRepository.findOne({ where: { url } });
    if (!exist) return false;
    const today = dayjs();
    const expire = today
      .add(toMax ? this.configService.get('OSS_MAX_DATE') : 1, 'day')
      .format('YYYYMMDD');
    try {
      await this.fileRepository.update(exist.id, { expire });
      return true;
    } catch (e) {}

    return false;
  }

  /**
   * 判断OSS是否存在url文件
   *
   * @param {string} url 文件地址
   * @return {*}  {Promise<boolean>}
   * @memberof FileService
   */
  async fileExist(url: string): Promise<boolean> {
    try {
      await this.client.head(url);
      return true;
    } catch (error) {
      if (error.code === 'NoSuchKey') return false;
    }

    return false;
  }

  /** 获取文件流
   *
   *
   * @param {string} url
   * @return {*}
   * @memberof FileService
   */
  async getFileStream(url: string): Promise<any> {
    try {
      if (!url) throw new BadRequestException(`参数错误`);
      const isOssFile = await this.fileExist(url);

      if (isOssFile) {
        const res = await this.client.get(url);
        return res?.content || null;
      } else {
        return await this.getBufferFromUrl(url);
      }
    } catch (e) {
      throw new BadRequestException(e.message);
    }

    return null;
  }

  /**
   * 定时任务删除OSS文件
   * 每天0点执行
   *
   * @memberof FileService
   */
  async clearExpireFiles() {
    try {
      const today = dayjs().format('YYYYMMDD');
      const listFile = await this.fileRepository.find({ where: { expire: today } });
      const names = listFile.map((o: any) => o.url).filter(Boolean);
      const ids = listFile.map((o: any) => o.id);

      if (listFile && listFile.length) {
        // 删除OSS文件
        await this.client.deleteMulti(names, { quiet: true });
        // 删除文件夹
        const child = await this.client.list({ prefix: `${today}/` });
        if (1 === child?.objects?.length) await this.client.delete(`${today}/`);
        // 删除MySQL记录
        await this.fileRepository.delete({ id: In(ids) });
      }
    } catch (e) {
      console.log(`清理暂存文件失败：${e.message}`);
    }
  }

  /**
   * 下载OSS文件
   *
   * @memberof FileService
   */
  async downloadFile(url: string) {
    // 秒
    const suffix = url.split('.').pop() || '';
    const fileName = `TYM_${getRandomStr(8, false)}`;
    const result = await this.client.signatureUrlV4(
      'GET',
      60,
      {
        headers: {
          'response-content-disposition': `attachment; filename="${fileName}.${suffix}"`,
        },
      },
      url,
    );

    return result;
  }

  // 新增定时任务
  addJob(data: TaskName, expression: CronExpression) {
    const { name } = data || {};
    const job = new CronJob(expression, () => {
      this.clearExpireFiles(); // 清理过期文件
    });

    this.schedulerRegistry.addCronJob(name, job);
    job.start();
  }
}
